﻿//
// © Algobox Ltd - Benn Eichhorn
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Algobox.Toolbox.Collections;
using Algobox.Objects.Finance.Assets.Stock;
using System.ComponentModel;
using Algobox.Feeds.Finance.MarketData;
using Algobox.Toolbox.Collections.TimeSeries;
using System.Threading;

namespace Algobox.Structure.Finance.Strategies.Dastan
{
    public class DastanStock : Strategy, INotifyPropertyChanged, IMarketUpdateLast, IDastanUnitCallback
    {
        #region Constructors

        public DastanStock(DastanManagement manager,
            Algobox.Feeds.Finance.Trading.ITradingFeedAdaptor tradingFeed,
            StockSecurity security,
            Guid databaseId,
            string account,
            int unitValue,
            int numEntryBars,
            int numExitBars,
            int movingAveragePeriod,
            int ticksPerBar)
            : base(security)
        {
            _isEnabled = false;
            _manager = manager;
            _tradingFeed = tradingFeed;
            DatabaseId = databaseId;
            Account = account;
            _security = security;
            _unitValue = unitValue;
            _numEntryBars = numEntryBars;
            _numExitBars = numExitBars;
            _periodMovingAverage = movingAveragePeriod;
            _ticksPerBar = ticksPerBar;
            _id = _numEntryBars + ":" + _numExitBars + ":" + _periodMovingAverage + ":" + _ticksPerBar;
            
            Phase = Phases.Initialised;
            _averageFilter = AverageFilter.Flat;

            if (security.Currency == Objects.Finance.Currencies.Currency.GBX)
                _divisor = 100;
            else
                _divisor = 1;

            Log.Info("{0}|{1}|TerrapinStock Creating Terrapin strategy with a unit value of {2} with Account({3}) DatabaseId({4})", _security, _id, unitValue, account, databaseId);
        }

        #endregion
        //////////
        #region Member Variables

        const int BAR_SIZE = 5; // number of minutes for each bar

        private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();

        public event PropertyChangedEventHandler PropertyChanged;

        private DastanManagement _manager;
        private Algobox.Feeds.Finance.Trading.ITradingFeedAdaptor _tradingFeed;
        string _id;
        private Phases _phase;
        private BarLookBack _barLookBack;
        private OHLCMinuteBars _bars1min;
        private object _lock = new object();

        private bool _isEnabled;
        private string _account;
        private int _unitValue;
        private int _numEntryBars;
        private int _numExitBars;
        private int _periodMovingAverage;
        private int _ticksPerBar;
        private int _divisor;

        private int _ticks;
        private double _last;
        private double _high;
        private double _low;
        private double _currentAverage;
        private double _previousAverage;
        private int _averageBarIndex;
        private int _channelBarIndex;
        private bool _isExiting;

        private AverageFilter _averageFilter;

        private DastanUnit _triggeredUnit;

        #endregion
        //////////
        #region Properties

        public Guid DatabaseId { get; private set; }
        public string Name { get { return Security.ToString() + "-" + _id; } }

        public bool IsEnabled
        {
            get { return _isEnabled; }
            set
            {
                lock (_lock)
                    _isEnabled = value;

                if (value && _phase == Phases.FillFailure)
                {
                    Log.Info("{0}|{1}|IsEnabled User has re-enabled after a fill failure", _security, _id);
                    _phase = Phases.BuildingData;
                }

                Log.Info("{0}|{1}|IsEnabled {2} ", _security, _id, value ? "enabled" : "disabled");
                NotifyPropertyChanged("IsEnabled");
            }
        }

        public string Account
        {
            get { return _account; }
            set
            {
                if (Log.IsInfoEnabled)
                    Log.Info("{0}|{1}|Account changing from {2} to {3}", _security, _id, _account, value);

                lock (_lock)
                    _account = value;

                NotifyPropertyChanged("Account");
            }
        }

        public StockSecurity Security { get { return _security; } }

        public int UnitValue { get { return _unitValue; } set { Log.Info("{0}|{1}|UnitValue changing from {2} to {3}", _security, _id, _unitValue, value); _unitValue = value; NotifyPropertyChanged("UnitValue"); } }
        public Phases Phase { get { return _phase; } private set { Log.Info("{0}|{1}|Phase changing from {2} to {3}", _security, _id, _phase, value); _phase = value; NotifyPropertyChanged("Phase"); } }
        public string Sector { get { return _security.Sector; } }

        public int EntryBars { get { return _numEntryBars; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|EntryBars changing from {2} to {3}", _security, _id, _numEntryBars, value); _numEntryBars = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodMovingAverage + ":" + _ticksPerBar; NotifyPropertyChanged("EntryBars"); NotifyPropertyChanged("Security"); } }
        public int ExitBars { get { return _numExitBars; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|ExitBars changing from {2} to {3}", _security, _id, _numExitBars, value); _numExitBars = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodMovingAverage + ":" + _ticksPerBar; NotifyPropertyChanged("ExitBars"); NotifyPropertyChanged("Security"); } }
        public int AveragePeriod { get { return _periodMovingAverage; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|FastEMA changing from {2} to {3}", _security, _id, _periodMovingAverage, value); _periodMovingAverage = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodMovingAverage + ":" + _ticksPerBar; NotifyPropertyChanged("FastEMA"); NotifyPropertyChanged("Security"); } }
        public int TicksPerBar { get { return _ticksPerBar; } }

        public int Ticks { get { return _ticks; } }
        public double Last { get { return _last; } private set { _last = value; NotifyPropertyChanged("Last"); } }
        public double High { get { return _high; } private set { _high = value; NotifyPropertyChanged("High"); } }
        public double Low { get { return _low; } private set { _low = value; NotifyPropertyChanged("Low"); } }
        public double MAFast { get { return _currentAverage; } }
        public double MASlow { get { return _previousAverage; } }
        public AverageFilter Filter { get { return _averageFilter; } private set { _averageFilter = value; NotifyPropertyChanged("Filter"); } }

        #endregion
        //////////
        #region Functions

        public override string ToString()
        {
            return Security.ToString() + "-" + _id;
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public void OnPriceLast(IStock stock, Objects.Finance.Prices.PriceVolume last)
        {
            if (Monitor.TryEnter(_lock, 10))
            {
                try
                {
                    //if(last.TimeStamp.TimeOfDay >= new TimeSpan(9,40,0))
                    //{
                    //    Log.Info("{0}|{1}|OnPriceLast Initialising price bars to {2} minutes between {3} and {4}", _security, _id, _bars.BarSize, _bars.StartTime, _bars.EndTime);
                    //}
                    int averageBarIndex;
                    if (_bars1min == null)
                    {
                        // bar look back 
                        _barLookBack = new BarLookBack(_numEntryBars, _ticksPerBar);

                        // initialiase bars to begin when the first datapoint arrives
                        _bars1min = new OHLCMinuteBars(last.TimeStamp, last.TimeStamp.Date.AddHours(16.5), TimeSpan.FromMinutes(1));

                        if (Log.IsInfoEnabled)
                        {
                            Log.Info("{0}|{1}|OnPriceLast Initialising price bars to {2} bars of {3} ticks per bar ", _security, _id, _barLookBack.NumBars, _barLookBack.BarSize);
                        }

                        _bars1min.TryAdd(last.TimeStamp, last.Price, last.Volume, out averageBarIndex);
           
                    }
                    else
                    {
                        if (!_bars1min.TryAdd(last.TimeStamp, last.Price, last.Volume, out averageBarIndex))
                        {
                            Phase = Phases.DoneForTheDay;
                            return;
                        }
                    }


                    if (_phase == Phases.Initialised)
                    {
                        Phase = Phases.BuildingData;
                    }
                    else if (_phase == Phases.BuildingData)
                    {
                        // check if we have enough data built                            
                        if (_barLookBack.HasData)
                        {
                            Phase = Phases.Analysing;
                            GetFilter(averageBarIndex);
                        }
                    }

                    if (_phase == Phases.Analysing)
                    {
                        // get current channel
                        if (averageBarIndex != _averageBarIndex)
                        {
                            _averageBarIndex = averageBarIndex;

                            // save previous moving average value
                            _previousAverage = _currentAverage;
                            GetFilter(_averageBarIndex);
                        }
                        // get new filter if required                            
                        else if (last.Price != _last)
                        {
                            GetFilter(_averageBarIndex);
                        }

                        //// BETA VERSION TO USE LAST
                        if (_barLookBack.HighCurrent > _barLookBack.High && _barLookBack.LowCurrent < _barLookBack.Low)
                        {
                            if (Log.IsInfoEnabled)
                                Log.Info("{0}|{1}|OnPriceLast Filtered price as current bar is both higher than high and lower than low, waiting till next bar CurrentHigh({2}) CurrentLow({3}) High({4}) Low({5})", _security, _id, _barLookBack.HighCurrent, _barLookBack.LowCurrent, _barLookBack.High, _barLookBack.Low);

                            Phase = Phases.BuildingData;
                        }
                        else
                        {
                            CheckBuy(last);
                            CheckSell(last);
                        }
                        //// BETA VERSION TO USE LAST
                    }
                    else if (_phase == Phases.Triggered)
                    {
                        System.Diagnostics.Debug.Assert(_triggeredUnit != null, "Phase is triggered, but unit is null");

                        if (last.TimeStamp.TimeOfDay >= DastanManagement.TIME_EXIT_ALL && !_isExiting)
                        {
                            _isExiting = true;

                            if (Log.IsInfoEnabled)
                                Log.Info("{0}|{1}|OnPriceLast Triggered order has reached it's exit time", _security, _id);

                            _triggeredUnit.ExitUnit(last.TimeStamp);

                        }
                        else
                        {
                            _triggeredUnit.OnBestPrice(last.Price, last.TimeStamp);
                        }
                    }


                    /*
                    switch (_phase)
                    {
                        case Phases.Initialised:
                            Phase = Phases.BuildingData;
                            // do something with the data
                            break;

                        case Phases.BuildingData:
                            // check if we have enough data built                            
                            if (_barLookBack.HasData)
                            {
                                Phase = Phases.Analysing;
                                GetFilter(averageBarIndex);
                            }
                            break;

                        case Phases.Analysing:
                            // get current channel
                            if (averageBarIndex != _averageBarIndex)
                            {
                                _averageBarIndex = averageBarIndex;

                                // save previous moving average value
                                _previousAverage = _currentAverage;
                                GetFilter(_averageBarIndex);
                            }
                            // get new filter if required                            
                            else if (last.Price != _last)
                            {
                                GetFilter(_averageBarIndex);
                            }
                            
                            //// BETA VERSION TO USE LAST
                            if (_barLookBack.HighCurrent > _barLookBack.High && _barLookBack.LowCurrent < _barLookBack.Low)
                            {
                                if (Log.IsInfoEnabled)
                                    Log.Info("{0}|{1}|OnPriceLast Filtered price as current bar is both higher than high and lower than low, waiting till next bar CurrentHigh({2}) CurrentLow({3}) High({4}) Low({5})", _security, _id, _barLookBack.HighCurrent, _barLookBack.LowCurrent, _barLookBack.High, _barLookBack.Low);

                                Phase = Phases.BuildingData;
                            }
                            else
                            {
                                CheckBuy(last);
                                CheckSell(last);
                            }
                            //// BETA VERSION TO USE LAST

                            break;

                        case Phases.Triggered:
                            System.Diagnostics.Debug.Assert(_triggeredUnit != null, "Phase is triggered, but unit is null");

                            if (last.TimeStamp.TimeOfDay >= DastanManagement.TIME_EXIT_ALL && !_isExiting)
                            {
                                _isExiting = true;

                                if (Log.IsInfoEnabled)
                                    Log.Info("{0}|{1}|OnPriceLast Triggered order has reached it's exit time", _security, _id);

                                _triggeredUnit.ExitUnit(last.TimeStamp);

                            }
                            else
                            {
                                _triggeredUnit.OnBestPrice(last.Price, last.TimeStamp);
                            }
                            break;
                    } // switch
                    */


                    // add latest tick price
                    int channelBarIndex = _channelBarIndex;
                    if (last.Price != _last)
                    {
                        _ticks++;
                        NotifyPropertyChanged("Ticks");
                        channelBarIndex = _barLookBack.Add(last.TimeStamp, last.Price, last.Volume);
                    }

                    if (channelBarIndex != _channelBarIndex)
                    {
                        _channelBarIndex = channelBarIndex;
                        
                        if (Log.IsDebugEnabled)
                            Log.Debug("{0}|{1}|GetChannel has updated the channels to High:{2:F2} Low:{3:F2} ", _security, _id, _barLookBack.High, _barLookBack.Low);

                        if (_triggeredUnit != null)
                        {
                            double high, low;
                            if (_barLookBack.TryGetPreviousHighLow(_numExitBars, out high, out low))
                            {
                                if (!_isExiting)
                                {
                                    if (!_triggeredUnit.IsBuy)
                                    {
                                        _triggeredUnit.ExitTarget = high;
                                        High = high;
                                    }
                                    else if (_triggeredUnit.IsBuy)
                                    {
                                        _triggeredUnit.ExitTarget = low;
                                        Low = low;
                                    }
                                }
                            }
                        }
                        else
                        {
                            High = _barLookBack.High;
                            Low = _barLookBack.Low;
                        }
                    }

                    // update the last price for this Stock
                    Last = last.Price;

                }
                finally
                {   
                    Monitor.Exit(_lock);
                }
            }
            else if (Log.IsTraceEnabled)
            {
                Log.Trace("{0}|{1}|OnPriceLast couldn't enter lock after 10 milliseconds", _security, _id);
            }
        }

        private void CheckBuy(Objects.Finance.Prices.PriceVolume price)
        {
            // check if we have a trigger
            if (_phase == Phases.Analysing
                && _averageFilter == AverageFilter.Buy
                && _isEnabled
                && price.TimeStamp.TimeOfDay < DastanManagement.TIME_LAST_ENTRY)
            {
                if (_high == _low)
                {
                    if (Log.IsInfoEnabled)
                    {
                        Log.Info("{0}|{1}|CheckBuyEntry Filtered short entry at {2} due to channel range being 0 High({3}) Low({4})", _security, _id, price.Price, _barLookBack.High, _barLookBack.Low);
                    }
                    return;
                }

                if (price.Price > _high)
                {   
                    if (_isEnabled)
                    {                        
                        double high, exitPrice;
                        if (_barLookBack.TryGetPreviousHighLow(_numExitBars, out high, out exitPrice))
                        {
                            Phase = Phases.Triggered;
                            uint quantity = (uint)(_unitValue / (price.Price / _divisor));
                            Log.Info("{0}|{1}|CheckBuyEntry Entering long {2}@{3} High({4})", _security, _id, quantity, price.Price, _barLookBack.High);
                            _triggeredUnit = new DastanUnit(this, _tradingFeed, this, true, price.Price, exitPrice, quantity);
                            _manager.AddStrategyUnit(_triggeredUnit);
                            _triggeredUnit.EnterUnit(price.TimeStamp);

                            Low = exitPrice;
                        }
                        else
                        {
                            Log.Debug("{0}|{1}|CheckBuyEntry Couldn't retrieve the previous high & low for exit", _security, _id, price.Price);
                        }
                    }
                    else
                    {
                        Log.Debug("{0}|{1}|CheckBuyEntry Disabled so filtered long entry at {2}", _security, _id, price.Price);
                    }
                }
            }
            else if (_phase == Phases.Triggered)
            {
                if (price.TimeStamp.TimeOfDay >= DastanManagement.TIME_EXIT_ALL && !_isExiting)
                {
                    Log.Info("{0}|{1}|CheckBuyEntry Exiting triggered unit due to time > than exit time {2}", _security, _id, DastanManagement.TIME_EXIT_ALL);
                    _triggeredUnit.ExitUnit(price.TimeStamp);
                }
                else if (_averageFilter == AverageFilter.Sell
                && _triggeredUnit != null)
                {
                    _triggeredUnit.OnBestPrice(price.Price, price.TimeStamp);
                }
            }
        }

        private void CheckSell(Objects.Finance.Prices.PriceVolume price)
        {
            // check if we have a trigger
            if (_phase == Phases.Analysing
                && _averageFilter == AverageFilter.Sell
                && _isEnabled
                && price.TimeStamp.TimeOfDay < DastanManagement.TIME_LAST_ENTRY)
            {
                if (price.Price < _low)
                {
                    if (_high == _low)
                    {
                        if (Log.IsInfoEnabled)
                        {
                            Log.Info("{0}|{1}|CheckSellEntry Filtered short entry at {2} due to channel range being 0 High({3}) Low({4})", _security, _id, price.Price, _barLookBack.High, _barLookBack.Low);
                        }
                        return;
                    }
                    
                    if (_isEnabled)
                    {
                        double exitPrice, low;
                        if (_barLookBack.TryGetPreviousHighLow(_numExitBars, out exitPrice, out low))
                        {
                            Phase = Phases.Triggered;
                            uint quantity = (uint)(_unitValue / (price.Price / _divisor));
                            Log.Info("{0}|{1}|CheckSellEntry Entering short {2}@{3} BarHigh({4})", _security, _id, quantity, price.Price, _barLookBack.Low);
                            _triggeredUnit = new DastanUnit(this, _tradingFeed, this, false, price.Price, exitPrice, quantity);
                            _manager.AddStrategyUnit(_triggeredUnit);
                            _triggeredUnit.EnterUnit(price.TimeStamp);

                            High = exitPrice;
                        }
                        else
                        {
                            Log.Debug("{0}|{1}|CheckSellEntry Couldn't retrieve the previous high & low for exit", _security, _id, price.Price);
                        }

                    }
                    else
                    {
                        Log.Debug("{0}|{1}|CheckSellEntry Disabled so filtered short entry at {2}", _security, _id, price.Price);
                    }                    
                }
            }
            else if (_phase == Phases.Triggered)
            {
                if (price.TimeStamp.TimeOfDay >= DastanManagement.TIME_EXIT_ALL && !_isExiting)
                {
                    Log.Info("{0}|{1}|CheckSellEntry Exiting triggered unit due to time > than exit time {2}", _security, _id, DastanManagement.TIME_EXIT_ALL);
                    _triggeredUnit.ExitUnit(price.TimeStamp);
                }
                else if (_averageFilter == AverageFilter.Buy
                && _triggeredUnit != null)
                {
                    _triggeredUnit.OnBestPrice(price.Price, price.TimeStamp);
                }
            }

        }

        public void ExitUnit(DateTime timeStamp)
        {
            if (Monitor.TryEnter(_lock, 1000))
            {
                try
                {
                    if (_phase == Phases.Triggered && _triggeredUnit != null && !_isExiting)
                    {
                        if (Log.IsInfoEnabled)
                            Log.Info("{0}|{1}|ExitUnit exiting currently triggered unit at market", _security, _id);

                        _triggeredUnit.ExitUnit(timeStamp);
                    }
                    else if (Log.IsTraceEnabled)
                    {
                        Log.Trace("{0}|{1}|ExitUnit can not exit when not triggered", _security, _id);
                    }
                }
                finally
                {
                    Monitor.Exit(_lock);
                }
            }
            else if (Log.IsDebugEnabled)
            {
                Log.Debug("{0}|{1}|ExitUnit couldn't enter lock after 1 second", _security, _id);
            }
        }
        
        
        private void GetFilter(int barIndex)
        {
            _currentAverage = _bars1min.GetMovingAverage(barIndex, _periodMovingAverage * BAR_SIZE, OHLCMinuteBars.Data.Close, OHLCMinuteBars.MAType.SMA);
            if (_previousAverage == 0)
            {
                _previousAverage = _currentAverage;
            }
   
            NotifyPropertyChanged("MAFast");
            NotifyPropertyChanged("MASlow");

            if (_currentAverage > _previousAverage)
            {
                if (_averageFilter != AverageFilter.Buy)
                {
                    if (Log.IsDebugEnabled)
                    {
                        Log.Debug("{0}|{1}|GetFilter has updated from {2} to Buy Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _currentAverage, _previousAverage);
                    }
                    Filter = AverageFilter.Buy;
                }
            }
            else if (_currentAverage < _previousAverage)
            {
                if (_averageFilter != AverageFilter.Sell)
                {
                    if (Log.IsDebugEnabled)
                    {
                        Log.Debug("{0}|{1}|GetFilter has updated from {2} to Sell Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _currentAverage, _previousAverage);
                    }
                    Filter = AverageFilter.Sell;
                }
            }
            else if (_averageFilter != AverageFilter.Flat)
            {
                if (Log.IsDebugEnabled)
                {
                    Log.Debug("{0}|{1}|GetFilter has updated from {2} to Flat Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _currentAverage, _previousAverage);
                }
                Filter = AverageFilter.Flat;
            }
        }
        

        public void OnTerrapinUnitComplete(DastanUnit unit)
        {
            lock (_lock)
            {
                if (Log.IsInfoEnabled)
                    Log.Info("{0}|{1}|OnTerrapinUnitComplete has recieved a completed unit with a profit of {2}", _security, _id, unit.Profit);

                // get the channel that includes the current bar
                //GetChannel(_barIndex + 1);
                //_barsInitCount -= 1;

                Phase = Phases.BuildingData;
                _triggeredUnit = null;
                _isExiting = false;
            }
        }

        public void OnTerrapinUnitFailed(DastanUnit unit)
        {
            lock (_lock)
            {
                if (Log.IsInfoEnabled)
                    Log.Info("{0}|{1}|OnTerrapinUnitFailed has failed to fill a unit so is disabling this unit", _security, _id, unit.Profit);

                IsEnabled = false;
                                
                Phase = Phases.FillFailure;
                _triggeredUnit = null;
            }
        }

        #endregion
        //////////
        #region Enums

        public enum Phases
        {
            Initialised,
            BuildingData,
            Analysing,
            Triggered,
            FillFailure,
            DoneForTheDay
        }

        public enum AverageFilter
        {
            Buy,
            Sell,
            Flat
        }

        #endregion

    }
}